/*
 * Copyright (C) 2008 Peter Kling
 * 
 * This file is part of 'A Kind of Billiard', a graphical billiard simulation.
 * 
 * 'A Kind of Billiard' is free software: you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 * 
 * 'A Kind of Billiard' is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with 'A Kind of Billiard'. If not, see
 * <http://www.gnu.org/licenses/>.
 */

package de.akob.misc;

import java.awt.Frame;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.media.opengl.DebugGL;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLCapabilities;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.glu.GLU;

import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.GLUT;

import de.akob.mathematics.MathVector;
import de.akob.mathematics.Mathematics;

public abstract class JOGLWindowWithMouseNavigation extends Frame implements GLEventListener, MouseListener,
        MouseMotionListener, MouseWheelListener {
	// generate glu and glut objects
	protected GLU        glu       = new GLU();
	protected GLUT       glut      = new GLUT();
	
	// camera position/orientation
	protected MathVector cameraPos = new MathVector(0., 0., -10.);
	protected double     rotx_camera = 0., roty_camera = 0., rotz_camera = 0.;
	
	// we use a frames-per-second animator
	private FPSAnimator  animator;
	
	// mouse coordinates from last mouse event
	protected int        x_mouse_prev, y_mouse_prev;
	
	/**
	 * Standard constructor.
	 */
	public JOGLWindowWithMouseNavigation() {
		// request bits for stencil buffer
		GLCapabilities glCapabilities = new GLCapabilities();
		glCapabilities.setStencilBits(8);
		
		// generate canvas, link it to the OpenGL context and add it to this window
		GLCanvas canvas = new GLCanvas(glCapabilities);
		canvas.addGLEventListener(this);
		add(canvas);
		
		// generate animator
		animator = new FPSAnimator(canvas, 60);
		
		// stop rendering and shut down the application when window is closed
		addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing(WindowEvent e) {
				animator.stop();
				System.exit(0);
			}
		});
	}
	
	/**
	 * Applies camera translation and rotation to the current OpenGL matrix.
	 */
	protected void applyViewTransformation(GL gl) {
		gl.glTranslated(cameraPos.get(1), cameraPos.get(2), cameraPos.get(3));
		gl.glRotated(rotx_camera, 1., 0., 0.);
		gl.glRotated(roty_camera, 0., 1., 0.);
		gl.glRotated(rotz_camera, 0., 0., 1.);
	}
	
	/**
	 * Display method for OpenGL.
	 */
	public abstract void display(GLAutoDrawable drawable);
	
	/**
	 * Support for on-the-fly screen mode switching (not yet implemented by jogl).
	 */
	public void displayChanged(GLAutoDrawable drawable, boolean modeChanged, boolean deviceChanged) { /* void */
	}
	
	/**
	 * Initializes the OpenGL context.
	 */
	public void init(GLAutoDrawable drawable) {
		drawable.setGL(new DebugGL(drawable.getGL())); // use the debug pipeline during development
		
		// register some input listeners for user interaction
		drawable.addMouseListener(this);
		drawable.addMouseMotionListener(this);
		drawable.addMouseWheelListener(this);
	}
	
	public void mouseClicked(MouseEvent e) { /* void */
	}
	
	/**
	 * For navigation using the mouse.
	 */
	public void mouseDragged(MouseEvent e) {
		// get current mouse coordinates
		int x_mouse = e.getX();
		int y_mouse = e.getY();
		double width_window = e.getComponent().getSize().getWidth();
		double height_window = e.getComponent().getSize().getHeight();
		
		// rotation
		if ((e.getModifiers() & InputEvent.BUTTON1_MASK) != 0) {
			rotx_camera += 360. * (y_mouse - y_mouse_prev) / height_window;
			roty_camera += 360. * (x_mouse - x_mouse_prev) / width_window;
		}
		
		// translation
		if ((e.getModifiers() & InputEvent.BUTTON3_MASK) != 0) {
			double transX = 5. * (x_mouse - x_mouse_prev) / width_window;
			double transY = -5. * (y_mouse - y_mouse_prev) / height_window;
			MathVector tx = Mathematics.mul(transX, Mathematics.getUnitVector(3, 1));
			MathVector ty = Mathematics.mul(transY, Mathematics.getUnitVector(3, 2));
			cameraPos.add(tx);
			cameraPos.add(ty);
		}
		
		// update previous mouse coordinates for next event
		x_mouse_prev = x_mouse;
		y_mouse_prev = y_mouse;
	}
	
	public void mouseEntered(MouseEvent e) { /* void */
	}
	
	public void mouseExited(MouseEvent e) { /* void */
	}
	
	public void mouseMoved(MouseEvent e) { /* void */
	}
	
	/**
	 * For navigation using the mouse.
	 */
	public void mousePressed(MouseEvent e) {
		x_mouse_prev = e.getX();
		y_mouse_prev = e.getY();
	}
	
	public void mouseReleased(MouseEvent e) { /* void */
	}
	
	/**
	 * For navigation using the mouse.
	 */
	public void mouseWheelMoved(MouseWheelEvent e) {
		double factor = Math.pow(1.005, e.getWheelRotation());
		cameraPos.set(3, cameraPos.get(3) * factor);
	}
	
	/**
	 * Called on a reshape event.
	 */
	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		GL gl = drawable.getGL();
		
		// set projection
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		glu.gluPerspective(30., 1., 0.1, 100.);
		gl.glMatrixMode(GL.GL_MODELVIEW);
	}
	
	@Override
	public void setVisible(boolean isVisible) {
		super.setVisible(isVisible);
		if (isVisible) {
			animator.start();
		} else {
			animator.stop();
		}
	}
}
